package jmine.tec.test.utils.spring;

import java.lang.reflect.Field;
import java.security.AccessController;
import java.security.PrivilegedExceptionAction;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.jdbc.Work;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.orm.hibernate3.SessionHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;

/**
 * EnhancedSpringTestCase that is aware of Hibernate
 * 
 * @author takeshi
 */
public abstract class EnhancedSpringTestCase extends BeanFactoryAwareTestCase {

    private Session session;

    private SessionFactory sf;

    /**
     * C'tor
     */
    public EnhancedSpringTestCase() {
        this(SPRING_BEANS_XML);
    }

    /**
     * C'tor
     * 
     * @param resource the Resource
     */
    public EnhancedSpringTestCase(final String resource) {
        this(resource, false);
    }

    /**
     * C'tor
     * 
     * @param resource the resource on witch to configure Spring BeanFactory
     * @param preferSingleton if true, will first lookup a BeanFactoryLocator before trying to create a new BeanFactory and the BeanFactory
     * will not be destroyed (should be closed manually)
     */
    public EnhancedSpringTestCase(final String resource, final boolean preferSingleton) {
        super(resource, false, preferSingleton);
    }

    /**
     * C'tor
     * 
     * @param beanRefFactory the beanRefFactory to be used
     * @param resource the resource on witch to configure Spring BeanFactory
     * @param preferSingleton if true, will first lookup a BeanFactoryLocator before trying to create a new BeanFactory and the BeanFactory
     * will not be destroyed (should be closed manually)
     */
    public EnhancedSpringTestCase(final String beanRefFactory, final String resource, final boolean preferSingleton) {
        super(beanRefFactory, resource, false, preferSingleton);
    }

    /**
     * Autowires fields by name, checking type compatibility
     */
    @Override
    public void autoWire() {
        Set<Field> fields = this.getAllFields();

        for (final Field field : fields) {
            final String name = field.getName();
            if (this.getBeanFactory().containsBean(name)) {
                Class<?> type = this.getBeanFactory().getType(name);
                if ((type != null) && field.getType().isAssignableFrom(type)) {
                    try {
                        AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
                            public Void run() throws Exception {
                                field.setAccessible(true);
                                field.set(EnhancedSpringTestCase.this, EnhancedSpringTestCase.this.getBean(name));
                                return null;
                            }
                        });
                    } catch (Exception e) {
                        System.err.println(e.getMessage());
                    }
                }
            }
        }
    }

    /**
     * Recupera todos os fields acessiveis nessa clase. Recupera os filds definidos nessa classe e em todas as super classes.
     * 
     * @return Set com todos os fields
     */
    private Set<Field> getAllFields() {
        Set<Field> fields = new HashSet<Field>();
        fields.addAll(Arrays.asList(this.getClass().getDeclaredFields()));

        Class<?> superClass = this.getClass().getSuperclass();
        while (superClass != null) {
            fields.addAll(Arrays.asList(superClass.getDeclaredFields()));
            superClass = superClass.getSuperclass();
        }
        return fields;
    }

    /**
     * Binds a Hibernate Session to the current Thread
     */
    public final void bindSession() {
        if (TransactionSynchronizationManager.hasResource(this.getSf())) {
            return;
        }
        Session s = this.getSession();
        s.beginTransaction();
        s.doWork(new Work() {
            public void execute(final Connection connection) throws SQLException {
                connection.setAutoCommit(false);
                connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
            }
        });
        this.bindSession(s);
    }

    /**
     * Binds the given session to the current transaction, opening the transaction.
     * 
     * @param s the {@link Session}
     */
    protected final void bindSession(final Session s) {
        Transaction tx = s.getTransaction();
        if (tx == null) {
            tx = s.beginTransaction();
        }
        SessionHolder sh = new SessionHolder(s);
        sh.setTransaction(tx);
        TransactionSynchronizationManager.bindResource(this.getSf(), sh);
    }

    /**
     * @return the session
     */
    public final Session getSession() {
        if (this.session == null) {
            try {
                this.session = this.getSf().getCurrentSession();
            } catch (HibernateException e) {
                this.session = this.getSf().openSession();
            }
            if (!this.session.isOpen()) {
                System.out.println(this.session);
            }
        }
        return this.session;
    }

    /**
     * @return the sf
     */
    private SessionFactory getSf() {
        if (this.sf == null) {
            if (this.getBeanFactory().containsBean("sessionFactory")
                    && SessionFactory.class.isAssignableFrom(this.getBeanFactory().getType("sessionFactory"))) {
                this.sf = (SessionFactory) this.getBeanFactory().getBean("sessionFactory");
                return this.sf;
            }
            String[] sfs = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.getBeanFactory(), SessionFactory.class);
            if (sfs.length > 0) {
                this.sf = (SessionFactory) this.getBeanFactory().getBean(sfs[0]);
            }
        }
        return this.sf;
    }

    /**
     * @param transaction the Transaction to rollback if its opened
     */
    private void rollbackTransaction(final Transaction transaction) {
        if ((transaction != null) && transaction.isActive()) {
            transaction.rollback();
        }
    }

    /**
     * Unbinds the Session associated to the current Thread if any, rolling back the transaction if it is not commited
     */
    public final void unbindSession() {
        if ((this.session != null) && TransactionSynchronizationManager.hasResource(this.getSf())) {
            if (this.session.isOpen()) {
                this.rollbackTransaction(this.session.getTransaction());
                if (this.session.isOpen()) {
                    this.session.disconnect();
                    this.session.close();
                }
            }
            TransactionSynchronizationManager.unbindResource(this.sf);
            this.session = null;
        }
    }

    /**
     * @throws Exception e
     */
    @Override
    protected void tearDownAfterClassInternal() throws Exception {
        super.tearDownAfterClassInternal();
        if (this.sf != null) {
            this.unbindSession();
        }
    }

    /**
     * Salva os objetos passados em ordem na sessão atual e faz o flush na sequência
     * 
     * @param objects os objetos a serem salvos
     */
    public void saveAndFlush(final Object... objects) {
        for (int i = 0; i < objects.length; i++) {
            this.getSession().save(objects[i]);
        }
        this.getSession().flush();
    }

    /**
     * Faz o flush na session
     */
    public void flushSession() {
        this.getSession().flush();
    }
}
